﻿using DevExpress.Data.Filtering;
using DevExpress.Data.Linq.Helpers;
using DevExpress.ExpressApp;
using DevExpress.Xpo;
using MongoDB.Driver;
using System.Reflection;

namespace EasyXaf.MongoDB;

public sealed class MongoObjectSpaceAdapter : IDisposable
{
    private readonly NonPersistentObjectSpace _objectSpace;
    private readonly MongoClientSettings _clientSettings;
    private readonly string _databaseName;

    private MongoClient _mongoClient;
    private IMongoDatabase _mongoDatabase;

    public MongoObjectSpaceAdapter(NonPersistentObjectSpace objectSpace, MongoClientSettings clientSettings, string databaseName)
    {
        _objectSpace = objectSpace;
        _clientSettings = clientSettings;
        _databaseName = databaseName;

        objectSpace.AutoSetModifiedOnObjectChange = true;

        objectSpace.ObjectsGetting += ObjectSpace_ObjectsGetting;
        objectSpace.ObjectGetting += ObjectSpace_ObjectGetting;
        objectSpace.ObjectByKeyGetting += ObjectSpace_ObjectByKeyGetting;
        objectSpace.ObjectReloading += ObjectSpace_ObjectReloading;
        objectSpace.ObjectsCountGetting += ObjectSpace_ObjectsCountGetting;
        objectSpace.CustomGetObjectsQuery += ObjectSpace_CustomGetObjectsQuery;
        objectSpace.ObjectSaving += ObjectSpace_ObjectSaving;
    }

    private static bool IsMongoEntity(Type objectType)
    {
        return typeof(MongoEntity).IsAssignableFrom(objectType);
    }

    private IMongoDatabase GetMongoDatabase()
    {
        if (_mongoDatabase == null)
        {
            _mongoClient = new MongoClient(_clientSettings);
            _mongoDatabase = _mongoClient.GetDatabase(_databaseName);
        }
        return _mongoDatabase;
    }

    private IMongoCollection<T> GetMongoCollection<T>()
    {
        return GetMongoDatabase().GetCollection<T>(GetEntityName(typeof(T)));
    }

    private static string GetEntityName(Type type)
    {
        var entityName = type.GetCustomAttribute<MongoEntityAttribute>()?.EntityName;
        if (string.IsNullOrWhiteSpace(entityName))
        {
            entityName = type.Name;
        }
        return entityName;
    }

    private T GetObjectT<T>(T obj)
        where T : MongoEntity
    {
        if (obj != null)
        {
            if (_objectSpace.IsNewObject(obj))
            {
                return obj;
            }

            var keyValue = _objectSpace.GetKeyValue(obj);
            var filter = Builders<T>.Filter.Eq(_objectSpace.GetKeyPropertyName(typeof(T)), keyValue);
            return GetMongoCollection<T>().Find(filter).FirstOrDefault();
        }

        return null;
    }

    private T GetObjectByKeyT<T>(object key)
        where T : MongoEntity
    {
        if (key != null)
        {
            var filter = Builders<T>.Filter.Eq(_objectSpace.GetKeyPropertyName(typeof(T)), key);
            return GetMongoCollection<T>().Find(filter).FirstOrDefault();
        }
        return null;
    }

    private void InsertObjectT<T>(T obj)
    {
        GetMongoCollection<T>().InsertOne(obj);
    }

    private void DeleteObjectT<T>(T obj)
    {
        var keyValue = _objectSpace.GetKeyValue(obj);
        var filter = Builders<T>.Filter.Eq(_objectSpace.GetKeyPropertyName(typeof(T)), keyValue);
        GetMongoCollection<T>().DeleteOne(filter);
    }

    private void UpdateObjectT<T>(T obj)
    {
        var keyValue = _objectSpace.GetKeyValue(obj);
        var filter = Builders<T>.Filter.Eq(_objectSpace.GetKeyPropertyName(typeof(T)), keyValue);
        GetMongoCollection<T>().ReplaceOne(filter, obj);
    }

    private object GetObject(object obj)
    {
        return GetType().GetMethod(nameof(GetObjectT), BindingFlags.NonPublic | BindingFlags.Instance)
            .MakeGenericMethod(obj.GetType())
            .Invoke(this, [obj]);
    }

    private object GetObjectByKey(Type objectType, object key)
    {
        return GetType().GetMethod(nameof(GetObjectByKeyT), BindingFlags.NonPublic | BindingFlags.Instance)
            .MakeGenericMethod(objectType)
            .Invoke(this, [key]);
    }

    private void InsertObject(object obj)
    {
        GetType().GetMethod(nameof(InsertObjectT), BindingFlags.NonPublic | BindingFlags.Instance)
            .MakeGenericMethod(obj.GetType())
            .Invoke(this, [obj]);
    }

    private void DeleteObject(object obj)
    {
        GetType().GetMethod(nameof(DeleteObjectT), BindingFlags.NonPublic | BindingFlags.Instance)
            .MakeGenericMethod(obj.GetType())
            .Invoke(this, [obj]);
    }

    private void UpdateObject(object obj)
    {
        GetType().GetMethod(nameof(UpdateObjectT), BindingFlags.NonPublic | BindingFlags.Instance)
            .MakeGenericMethod(obj.GetType())
            .Invoke(this, [obj]);
    }

    private IQueryable<T> GetObjectsQueryT<T>()
    {
        return GetMongoCollection<T>().AsQueryable();
    }

    private IQueryable GetObjectsQuery(Type objectType)
    {
        var method = GetType().GetMethod(nameof(GetObjectsQueryT), BindingFlags.NonPublic | BindingFlags.Instance);
        var genericMethod = method.MakeGenericMethod(objectType);
        return (IQueryable)genericMethod.Invoke(this, null);
    }

    private IQueryable GetQueryable(Type objectType, CriteriaOperator criteria = null, SortProperty[] sorting = null)
    {
        return new QueryableCollection(_objectSpace, objectType, criteria) { Sorting = sorting }.Queryable;
    }

    private void Collection_FetchObjects(object sender, FetchObjectsEventArgs e)
    {
        var collection = new QueryableCollection(_objectSpace, e.ObjectType, e.Criteria)
        {
            Sorting = e.Sorting?.ToArray(),
            TopReturnedObjectsCount = e.TopReturnedObjects,
        };
        e.Objects = collection.Queryable;
    }

    private void ObjectSpace_ObjectsGetting(object sender, ObjectsGettingEventArgs e)
    {
        if (IsMongoEntity(e.ObjectType))
        {
            var collection = new DynamicCollection(_objectSpace, e.ObjectType, e.Criteria, e.Sorting, e.InTransaction);
            collection.FetchObjects += Collection_FetchObjects;
            e.Objects = collection;
        }
    }

    private void ObjectSpace_ObjectGetting(object sender, ObjectGettingEventArgs e)
    {
        if (e.SourceObject != null && IsMongoEntity(e.SourceObject.GetType()))
        {
            e.TargetObject = GetObject(e.SourceObject);
        }
    }

    private void ObjectSpace_ObjectByKeyGetting(object sender, ObjectByKeyGettingEventArgs e)
    {
        if (IsMongoEntity(e.ObjectType))
        {
            e.Object = GetObjectByKey(e.ObjectType, e.Key);
        }
    }

    private void ObjectSpace_ObjectReloading(object sender, ObjectGettingEventArgs e)
    {
        if (e.SourceObject != null && IsMongoEntity(e.SourceObject.GetType()))
        {
            e.TargetObject = GetObject(e.SourceObject);
        }
    }

    private void ObjectSpace_ObjectsCountGetting(object sender, ObjectsCountGettingEventArgs e)
    {
        if (IsMongoEntity(e.ObjectType))
        {
            e.Count = GetQueryable(e.ObjectType, (CriteriaOperator)e.Criteria).Count();
        }
    }

    private void ObjectSpace_CustomGetObjectsQuery(object sender, CustomGetObjectsQueryEventArgs e)
    {
        if (IsMongoEntity(e.ObjectType))
        {
            e.Queryable = GetObjectsQuery(e.ObjectType);
        }
    }

    private void ObjectSpace_ObjectSaving(object sender, ObjectManipulatingEventArgs e)
    {
        if (e.Object is MongoEntity entity)
        {
            if (_objectSpace.IsNewObject(entity))
            {
                InsertObject(entity);
            }
            else if (_objectSpace.IsDeletedObject(entity))
            {
                DeleteObject(entity);
            }
            else
            {
                UpdateObject(entity);
            }
        }
    }

    public void Dispose()
    {
        if (_mongoClient != null)
        {
            _mongoClient.Dispose();
            _mongoClient = null;
        }

        _objectSpace.ObjectsGetting -= ObjectSpace_ObjectsGetting;
        _objectSpace.ObjectGetting -= ObjectSpace_ObjectGetting;
        _objectSpace.ObjectByKeyGetting -= ObjectSpace_ObjectByKeyGetting;
        _objectSpace.ObjectReloading -= ObjectSpace_ObjectReloading;
        _objectSpace.ObjectsCountGetting -= ObjectSpace_ObjectsCountGetting;
        _objectSpace.CustomGetObjectsQuery -= ObjectSpace_CustomGetObjectsQuery;
        _objectSpace.ObjectSaving -= ObjectSpace_ObjectSaving;
    }
}
